マンガデータの内訳を見る#

マンガデータを例に、内訳を見るためのデータビジュアライゼーション手法を学びましょう:

質的変数の内訳を可視化する際は、円グラフ棒グラフが用いられます。 前者は割合を直感的に表現する際に適しており、後者は絶対値を比較する際に適しています。 連続的に変化する内訳を表現する際は積上げ密度プロットが効果的です。 複数の質的変数の内訳を表現する際はモザイクプロットツリーマップ、そして パラレルセットグラフを検討しましょう。 特に、モザイクプロットは二つの質的変数の組合せの内訳を表現したいときに、ツリーマップは階層構造を持つ質的変数の内訳を表現したいときに、そしてパラレルセットグラフは三つ以上の質的変数の内訳を表現したいときに力を発揮します。

なお、データビジュアライゼーション手法の選定に関しては、Claus O. Wilke, Fundamentals of Data Visualization5.3 Proportionsを参考にしました。

初期設定#

以降では、マンガ・アニメ・ゲームデータを可視化するための初期設定を行います。 なお、紙幅の都合のため、書籍版と一部構成が異なることにご注意ください。

Import#

必要なライブラリをImportします。

Hide code cell content
# warningsモジュールのインポート
import warnings

# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Hide code cell content
# itertoolsモジュールのインポート
# 様々なパターンのループを効率的に実行可能
import itertools

# osモジュールのインポート
# オペレーティングシステムとのインターフェースを提供
import os

# pathlibモジュールのインポート
# ファイルシステムのパスを扱う
from pathlib import Path

# typingモジュールからの型ヒント関連のインポート
# 関数やクラスの引数・返り値の型を注釈するためのツール
from typing import Any, Dict, List, Optional, Union

# numpy:数値計算ライブラリのインポート
# npという名前で参照可能
import numpy as np

# pandas:データ解析ライブラリのインポート
# pdという名前で参照可能
import pandas as pd

# plotly.expressのインポート
# インタラクティブなグラフ作成のライブラリ
# pxという名前で参照可能
import plotly.express as px

# plotly.graph_objectsのインポート
# より詳細なグラフ作成機能を利用可能
# goという名前で参照可能
import plotly.graph_objects as go

# plotly.graph_objectsからFigureクラスのインポート
# 型ヒントの利用を主目的とする
from plotly.graph_objects import Figure

なお、型ヒントについてはこちらを参照ください。

定数#

本Notebookで用いる定数を定義します。 なお、Pythonにおける定数の扱いについては、こちらを参照ください。

Hide code cell content
# マンガデータが保存されているディレクトリのパス
DIR_IN = Path("../../data/cm/input")

# 分析結果の出力先ディレクトリのパス
DIR_OUT = DIR_IN.parent / "output" / Path.cwd().parts[-1] / "props"
Hide code cell content
# 読み込み対象ファイル名の定義

# マンガ各話に関するファイル
FN_CE = "cm_ce.csv"

# マンガ作品と原作者の対応関係に関するファイル
FN_CC_CRT = "cm_cc_crt.csv"
Hide code cell content
# 可視化に関する設定値を定義

# weekdayを曜日に変換するための辞書
WD2STR = {
    0: "月",
    1: "火",
    2: "水",
    3: "木",
    4: "金",
    5: "土",
    6: "日",
}
Hide code cell content
# plotlyの描画設定の定義

# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"
Hide code cell content
# 質的変数の描画用のカラースケールの定義

# Okabe and Ito (2008)基準のカラーパレット
# 色の識別性が高く、多様な色覚の人々にも見やすい色組み合わせ
# 参考URL: https://jfly.uni-koeln.de/color/#pallet
OKABE_ITO = [
    "#000000",  # 黒 (Black)
    "#E69F00",  # 橙 (Orange)
    "#56B4E9",  # 薄青 (Sky Blue)
    "#009E73",  # 青緑 (Bluish Green)
    "#F0E442",  # 黄色 (Yellow)
    "#0072B2",  # 青 (Blue)
    "#D55E00",  # 赤紫 (Vermilion)
    "#CC79A7",  # 紫 (Reddish Purple)
]

関数#

本Notebookで用いる関数を定義します。

Hide code cell content
def show_fig(fig: Figure) -> None:
    """
    所定のレンダラーを用いてplotlyの図を表示
    Jupyter Bookなどの環境での正確な表示を目的とする

    Parameters
    ----------
    fig : Figure
        表示対象のplotly図

    Returns
    -------
    None
    """

    # 図の周囲の余白を設定
    # t: 上余白
    # l: 左余白
    # r: 右余白
    # b: 下余白
    fig.update_layout(margin=dict(t=25, l=25, r=25, b=25))

    # 所定のレンダラーで図を表示
    fig.show(renderer=RENDERER)
Hide code cell content
def add_years_to_df(
    df: pd.DataFrame, unit_years: int = 10, col_date: str = "date"
) -> pd.DataFrame:
    """
    データフレームにunit_years単位で区切った年数を示す新しい列を追加

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    unit_years : int, optional
        年数を区切る単位、デフォルトは10
    col_date : str, optional
        日付を含むカラム名、デフォルトは "date"

    Returns
    -------
    pd.DataFrame
        新しい列が追加されたデータフレーム
    """

    # 入力データフレームをコピー
    df_new = df.copy()

    # unit_years単位で年数を区切り、新しい列として追加
    df_new["years"] = (
        pd.to_datetime(df_new[col_date]).dt.year // unit_years * unit_years
    )

    # 'years'列のデータ型を文字列に変更
    df_new["years"] = df_new["years"].astype(str)

    return df_new
Hide code cell content
def add_weekday_to_df(df: pd.DataFrame, col_date: str = "date") -> pd.DataFrame:
    """
    指定されたDataFrameに曜日の情報を追加する関数

    Parameters
    ----------
    df : pd.DataFrame
        曜日情報を追加する対象のDataFrame
    col_date : str, optional
        日付情報が含まれているカラムの名前、デフォルトは "date"

    Returns
    -------
    pd.DataFrame
        曜日情報が追加された新しいDataFrame
    """

    # 元のDataFrameをコピーして新しいDataFrameを作成
    df_new = df.copy()

    # 日付カラムを元に曜日の数値を計算して新しいカラムに追加
    df_new["weekday"] = pd.to_datetime(df_new[col_date]).dt.weekday

    # 数値の曜日を文字列に変換して新しいカラムに追加
    df_new["weekday_str"] = df_new["weekday"].apply(lambda x: WD2STR[x])

    return df_new
Hide code cell content
def create_mosaicplot(
    df: pd.DataFrame,
    x: str,
    y: str,
    color: str,
    width: str,
    text: str,
    color_discrete_sequence: List[str] = OKABE_ITO,
) -> go.Figure:
    """
    指定されたDataFrameを元にモザイクプロットを作成する関数

    Parameters
    ----------
    df : pd.DataFrame
        プロットに使用するデータが含まれるDataFrame
    x : str
        x軸に表示するデータのカラム名
    y : str
        y軸に表示するデータのカラム名
    color : str
        グループ分けの基準となるデータのカラム名
    width : str
        各バーの幅を表すデータのカラム名
    text : str
        各バーに表示するテキストのデータのカラム名
    color_discrete_sequence : List[str], optional
        使用する色のリスト デフォルトはOKABE_ITOのカラーパレット

    Returns
    -------
    go.Figure
        作成されたモザイクプロットのFigureオブジェクト
    """

    # 空のFigureオブジェクトを作成
    fig = go.Figure()

    # color列に登場するユニークな要素に対し、色をマッピング
    unique_keys = df[color].unique()
    color_map = {
        name: color for name, color in zip(unique_keys, color_discrete_sequence)
    }

    # color列のユニークな要素ごとにDataFrameをフィルタリング
    for i, name in enumerate(unique_keys):
        df_tmp = df[df[color] == name].reset_index(drop=True)
        # 幅をwidth列から抽出
        widths = df_tmp[width]

        # バーの位置を計算し、プロットに追加
        # 幅が変わるようxの値を調整
        fig.add_trace(
            go.Bar(
                name=name,
                x=df_tmp[width].cumsum() - widths,
                y=df_tmp[y],
                text=df_tmp[text],
                width=widths,
                offset=0,
                marker_color=color_map[name],
            )
        )

        # 最初の要素を用いて、X軸ラベルの設定値を作成
        if i == 0:
            # 各「棒」の中央に配置されるように座標を計算
            tickvals = df_tmp[width].cumsum() - df_tmp[width] / 2
            ticktext = df_tmp[x].unique()
            # x軸の表示範囲を決定するために利用
            x_max = df_tmp[width].sum()

    # x軸の目盛りの位置、テキスト、表示範囲を設定
    # 「棒」の太さの合計値を1としたとき、左右に0.1ずつ余白が残るように調整
    fig.update_xaxes(
        tickvals=tickvals, ticktext=ticktext, title=x, range=[-x_max * 0.1, x_max * 1.1]
    )

    # y軸のタイトルを設定
    fig.update_yaxes(title=y)

    # プロットのレイアウトを設定、凡例タイトルも指定
    fig.update_layout(barmode="stack", legend_title=color)

    return fig
Hide code cell content
def format_cols(df: pd.DataFrame, cols_rename: Dict[str, str]) -> pd.DataFrame:
    """
    指定されたカラムのみをデータフレームから抽出し、カラム名をリネームする関数

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    cols_rename : Dict[str, str]
        リネームしたいカラム名のマッピング(元のカラム名: 新しいカラム名)

    Returns
    -------
    pd.DataFrame
        カラムが抽出・リネームされたデータフレーム
    """

    # 指定されたカラムのみを抽出し、リネーム
    df = df[cols_rename.keys()].rename(columns=cols_rename)

    return df
Hide code cell content
def save_df_to_csv(df: pd.DataFrame, dir_save: Path, fn_save: str) -> None:
    """
    DataFrameをCSVファイルとして指定されたディレクトリに保存する関数

    Parameters
    ----------
    df : pd.DataFrame
        保存対象となるDataFrame
    dir_save : Path
        出力先ディレクトリのパス
    fn_save : str
        保存するCSVファイルの名前(拡張子は含めない)
    """
    # 出力先ディレクトリが存在しない場合は作成
    dir_save.mkdir(parents=True, exist_ok=True)

    # 出力先のパスを作成
    p_save = dir_save / f"{fn_save}.csv"

    # DataFrameをCSVファイルとして保存する
    df.to_csv(p_save, index=False, encoding="utf-8-sig")

    # 保存完了のメッセージを表示する
    print(f"DataFrame is saved as '{p_save}'.")

可視化例#

まず、可視化対象となるデータを読み込みましょう。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ce = pd.read_csv(DIR_IN / FN_CE)
df_cc_crt = pd.read_csv(DIR_IN / FN_CC_CRT)

円グラフ#

マンガ雑誌で連載を勝ち取ることは、至難の業です。

連載に至るためには、一般的に①マンガ賞に投稿する、②入賞する、③読切を掲載される、④連載会議に通るというステップを踏む必要があります[健 and つぐみ, 2009]。 なお、「読切」とは、連載でない1話完結の作品を指します。 一説[みのる, 2022]によると、各ステップで志望者の数は1/10程度まで絞られると言います。 つまり、①の段階で1万人いたとしても、④で最終的に連載を勝ち取るのは10人程度[1]になるということです。

本項ではこのマンガ業界の厳しさ、特に③から④に至る門の狭さを、円グラフを用いて確認します。 具体的には、「読切マンガ作品の掲載経験のあるマンガ作者のうち、連載に至るのは10%程度である」という仮説を確認[2]します。

円グラフPie Chart) とは、主に質的変数に対して、その内訳を 扇形の角度 で表した可視化手法です。 扇形グラフ とも呼ばれます。 質的変数の内訳を可視化する際に最もよく用いられる可視化手法の一つです。 それぞれの要素が全体に対してどの程度占めるか直感的にわかりやすいという長所がありますが、 一方で要素同士の内訳の比較がしづらいという短所もあります。 詳細は7章を参照ください。

まず、各マンガ作者が最初に掲載した 読切マンガ作品 を格納するDataFrameを作成します。 今回扱うデータには読切マンガであることを明示する情報はないため、便宜的に

  • 5ページ以上である[3]

  • 合計各話数が1話である

  • 合計作者数が1人である[4]

という全ての条件を満たすマンガ作品を読切マンガ作品と定義します。

Hide code cell content
# 5ページ以上掲載されたccidのリストを作成
ccids_by_pages = df_ce[df_ce["pages"] >= 5]["ccid"].unique()

# 合計マンガ作者数が1のccidのリストを作成するために、まずccid別のマンガ作者数を集計
df_ccid_ncrt = df_cc_crt.groupby("ccid")["crtid"].nunique().reset_index(name="n_crt")
# その上で、n_crt(マンガ作者数)が1の作品のみ抽出してリスト化
ccids_by_ncrt = df_ccid_ncrt[df_ccid_ncrt["n_crt"] == 1]["ccid"].unique()

# ccids_by_pagesとccids_by_ncrtの両方の条件を満たすccidsを作成
ccids = set(ccids_by_pages) & set(ccids_by_ncrt)

# 上記を満たすマンガ作品のうち、合計話数が1であるものを抽出
df_oneshot = df_cc_crt[
    (df_cc_crt["ccid"].isin(ccids)) & (df_cc_crt["n_ce"] == 1)
].reset_index(drop=True)
# マージ用に抽出する列
cols4merge = ["first_date", "ccid", "ccname", "mcname"]
# 更に、マンガ作者ごとに最も古いものを抽出するためにfirst_dateでソート
df_oneshot = df_oneshot.sort_values("first_date", ignore_index=True)
# crtid別にグルーピングし、先頭の行のcrtnameとcols4mergeのみ抽出
df_first_oneshot = (
    df_oneshot.groupby(["crtid"])[["crtname"] + cols4merge].first().reset_index()
)

# マージ用にカラム名を変更、先頭にoneshotをつける
df_first_oneshot = df_first_oneshot.rename(
    columns={c: f"oneshot_{c}" for c in cols4merge}
)

次に、各マンガ作者の最初の 連載マンガ作品 を格納するDataFrameを作成します。 今回扱うデータには連載マンガ作品であることを明示する情報はないため、便宜的に

  • 合計各話数が8話以上である

という条件を満たすマンガ作品を連載マンガ作品と定義します[5]。 閾値を8話とした理由は、マンガデータの基礎分析を参照ください。

Hide code cell content
# 合計各話数が8話以上のマンガ作品とマンガ作者の組合せを抽出
df_series = df_cc_crt[df_cc_crt["n_ce"] >= 8].reset_index(drop=True)
# 更に、マンガ作者ごとに最も古いものを抽出するためにfirst_dateでソート
df_series = df_series.sort_values("first_date", ignore_index=True)
# crtid別にグルーピングし、先頭の行のcols4mergeのみ抽出
df_first_series = df_series.groupby("crtid")[cols4merge].first().reset_index()

# マージ用にカラム名を変更、先頭にseriesをつける
df_first_series = df_first_series.rename(columns={c: f"series_{c}" for c in cols4merge})

上記二つのDataFrameをleft joinし、読切マンガ作品掲載後に連載マンガ作品を掲載されたと 考えられる マンガ作者数と、そうでないマンガ作者数を集計します。

Hide code cell content
# crtidを基準にdf_first_oneshotとdf_first_seriesをleft join
df_merge = pd.merge(df_first_oneshot, df_first_series, on="crtid", how="left")

# 読切マンガ作品掲載後に連載マンガ作品に繋がったと考えられるマンガ作者
df_crt_serialized = df_merge[
    df_merge["oneshot_first_date"] < df_merge["series_first_date"]
].reset_index(drop=True)
# そうでないと考えられるマンガ作者
df_crt_notyet = df_merge[df_merge["series_ccid"].isna()].reset_index(drop=True)

# 全ての読切マンガ掲載経験のあるマンガ作者をconcat、ただしignore_indexすることでindexを貼りなおす
df_crt_oneshot = pd.concat([df_crt_serialized, df_crt_notyet], ignore_index=True)
Hide code cell content
# 作図用に二つのDataFrameを集計
df_pie = pd.DataFrame(
    [
        {"連載化": "済", "マンガ作者数": df_crt_serialized["crtid"].nunique()},
        {"連載化": "未", "マンガ作者数": df_crt_notyet["crtid"].nunique()},
    ]
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_pie.head()
連載化 マンガ作者数
0 355
1 960
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_pie, DIR_OUT, "pie")
DataFrame is saved as '../../data/cm/output/02/props/pie.csv'.
Hide code cell source
# df_pieデータフレームを使用して、'マンガ作者数'を値とし、'連載化'を名前として円グラフを作成
# OKABE_ITOカラーシーケンスを使用して色を指定
fig = px.pie(
    df_pie,
    values="マンガ作者数",
    names="連載化",
    color_discrete_sequence=OKABE_ITO,
)

# 作成した円グラフを表示
show_fig(fig)

上図は、四大少年週刊誌において読切マンガ作品の掲載経験のあるマンガ作者のうち、四大少年週刊誌での連載に至ったマンガ作者とそうでないマンガ作者の割合を示した円グラフです。 ここで読切マンガ作品とは、「5ページ以上」かつ「合計話数が1話」かつ「作者数が1人」のマンガ作品を指し、連載マンガ作品とは「合計話数が8話以上」のマンガ作品を指します。 素直にこの上図を解釈すると、読切マンガ作品掲載から約27%ほどが連載に繋がっていることがわかります。 つまり、「読切マンガ作品を掲載されたマンガ作者のうち連載に至る割合は10%程度である」という仮説より多くのマンガ作者が連載を勝ち取っていそうに見えます。

この結果を解釈するにあたり、3点注意が必要です。 まず、上図の分母は 四大少年週刊誌において読切マンガ作品の掲載経験のあるマンガ作者 であり、この時点で(同一マンガ雑誌で連載を持つ可能性があるという観点で)有望な方々である、という点です。 つまり、 四大少年誌以外のマンガ雑誌を無視 した可視化になっています。 具体例を用いて示しましょう。

Hide code cell content
# マンガ作者名が遠藤達哉と一致するレコードを抽出
df_crt_oneshot[df_crt_oneshot["crtname"] == "遠藤達哉"].T
1214
crtid CCRT03052
crtname 遠藤達哉
oneshot_first_date 2000-12-04
oneshot_ccid C88617
oneshot_ccname 月華美人
oneshot_mcname 週刊少年ジャンプ
series_first_date NaN
series_ccid NaN
series_ccname NaN
series_mcname NaN

例えば、遠藤達哉さんは2000-12-04発売の週刊少年ジャンプ月華美人という読切マンガ作品を掲載しましたが、本書で扱うデータ中にはその後連載を勝ち取った記録はありませんでした。 しかし実際は、ジャンプSQ月華美人を連載化し、のちに少年ジャンプ+SPY×FAMILYを連載するに至ります。

次に注意すべきなのが、 出張掲載 の存在です。 出張掲載とは、あるマンガ雑誌の人気マンガ作品が(同じ出版社が発行する)別のマンガ雑誌に掲載される企画を指します。 今回の分析では、定義上 読切マンガ作品と出張マンガ作品を区別できません 。 以下の例を見てみましょう。

Hide code cell content
# マンガ作者名が山本崇一朗と一致するレコードを抽出
df_crt_oneshot[df_crt_oneshot["crtname"] == "山本崇一朗"].T
809
crtid CCRT01585
crtname 山本崇一朗
oneshot_first_date 2014-06-11
oneshot_ccid C92613
oneshot_ccname からかい上手の
oneshot_mcname 週刊少年サンデー
series_first_date NaN
series_ccid NaN
series_ccname NaN
series_mcname NaN

山本崇一朗さんは当時ゲッサン少年サンデーコミックスにてからかい上手の高木さんを連載しており、上記はその出張掲載にあたります。 マンガ作品名まで異なっているため、ドメイン知識がなければ読切マンガ作品と区別することは難しいでしょう。 上図の円グラフでは、山本崇一朗さんは連載の(読切マンガ作品を掲載したが、まだ連載に至っていない)マンガ作者として集計されてしまっています。 この問題を解決するためにも、分析対象とするマンガ雑誌の種類を増やす必要があります。

最後に、ヒストグラムでも指摘した通り、 データの打ち切り [6]の問題があります。 つまり、上記の分析では データの対象期間外に連載を獲得したマンガ作者 を適切に評価できません。 ここでも、ドメイン知識に基づいた具体例を示します。

Hide code cell content
# マンガ作者名が三浦糀と一致するレコードを抽出
df_crt_oneshot[df_crt_oneshot["crtname"].str.contains("三浦糀")].T
540
crtid CCRT00629
crtname 三浦糀
oneshot_first_date 2017-06-07
oneshot_ccid C115558
oneshot_ccname 先生、好きです。
oneshot_mcname 週刊少年マガジン
series_first_date NaN
series_ccid NaN
series_ccname NaN
series_mcname NaN

本書執筆時点の2024年1月19日において、三浦糀さんは週刊少年ジャンプにてアオのハコを連載中です。 しかし、本書で扱うマンガデータの対象期間内に連載を開始していなかったため、上図の円グラフでは連載側に集計されています。

この問題を根本的に解決するためには、各マンガ作品に対して、(本書で採用した便宜上の定義ではなく)読切作品を示す確定情報を追加する必要があります。 大きく以下二つの手段が考えられます:

  • 外部データの利用 :読切マンガ作品を情報を保有するデータを探し、本書のデータと結合する

  • データの自作 :読切マンガ作品か否か、 自分で一つ一つ判断 してデータを作成し、本書のデータと結合する

今回は紙幅の都合からどちらも実施できませんでした。 しかし、「外部データの利用」に関してはアニメデータの前処理にて、声優情報の補強のためにWikipediaの情報を用いています。 また、「データの作成」に関しては、メディア展開データの前処理にて、アニメ作品とその原作マンガ作品の対応付けを筆者自ら行います。 それぞれ題材は異なりますが、作業のイメージを掴むには十分だと思いますので、興味がある方は確認してみましょう。

積上げ棒グラフ#

狭き門をくぐり抜けて連載枠を勝ち取ったあとは、連載枠を守るための戦いが待っています。

ヒストグラム密度プロットでは、「長期連載作品とそうでない作品は、連載開始直後の数話の掲載位置の分布が異なる」という仮説を確認しました。 本節では、掲載位置以外でマンガ作品の人気を測る指標となりえる 4色カラーを獲得した各話数 に注目します。 なお、マンガ雑誌ではかつて 黒と赤による2色カラー も採用されていましたが、現在はほとんど使われていないため可視化対象から除外しました。 本章では特に断らない限り、「カラー」とは「4色カラー」を指します。

モノクロ印刷を基本とする週刊マンガ雑誌においては、4色カラーページは非常に貴重な有限の資源です。 純粋に印刷・製本に費用がかかる上に、マンガ作者にも大きな稼働がかかるため、スケジュールの調整も必要です。 よって、マンガ雑誌として多大なコストを払ってでも際立たせたい作品、つまり 人気マンガ作品ほど4色カラーを獲得しやすい と考えるのは自然です。 本項では積上げ棒グラフを用い、「長期連載作品とそうでない作品は、連載開始直後の数話の4色カラー獲得数が異なる」という仮説を確認します。 連載開始直後の数話 に注目した理由はヒストグラムと同様です。

積上げ棒グラフStacked Bar Chart ) とは、 新たな質的変数に応じて棒グラフの内訳を分割し、 直列に並べた 可視化手法です。 詳細は7章を参照ください。

ヒストグラムと同様、可視化対象を連載開始から8話目までとします。

Hide code cell content
# 連載マンガ作品として扱う最小のマンガ各話数を、マンガデータの基礎分析を踏まえ設定
min_nce = 8

# 'ccid'でグループ化し、各ccnameについてユニークなceidの数をカウント
df_cc_nce = df_ce.groupby(["ccid"])["ceid"].nunique().reset_index(name="n_ce")
# n_ceの値がmin_nce以上の行だけを保持
df_cc_nce = df_cc_nce[df_cc_nce["n_ce"] >= min_nce].reset_index(drop=True)
# n_ceの値でデータフレームを昇順にソート
df_cc_nce = df_cc_nce.sort_values("n_ce", ignore_index=True)
Hide code cell content
# df_ceを日付とceidでソートし、各ccidについて最初のmin_nce件のデータを取り出す
df_cc_n4c = (
    df_ce.sort_values(["date", "ceid"], ignore_index=True).groupby("ccid").head(min_nce)
)

# df_cc_nceのccid列に含まれるccidの行だけを保持
df_cc_n4c = df_cc_n4c[df_cc_n4c["ccid"].isin(df_cc_nce["ccid"].unique())]

# ccidごとにfour_coloredの数を集計
df_cc_n4c = (
    df_cc_n4c.groupby(["mcname", "ccname", "ccid"])["four_colored"]
    .sum()
    .reset_index(name="n_4c")
)

同様に、合計各話数の四分位値を用いてマンガ作品を分類します。

Hide code cell content
# min_nce、四分位数、最大値+1を使ってデータの範囲(threashold)を示すリストqs_ceを作成
# 後に登場するfor文をきれいに書くために、min_nceと、n_ceの最大値+1を追加
ths_ce = (
    [min_nce]  # min_nceをリストの最初に追加
    + list(df_cc_nce["n_ce"].quantile([0.25, 0.5, 0.75]).astype(int))  # 四分位数を追加
    + [df_cc_nce["n_ce"].max() + 1]  # 最大値+1をリストの最後に追加
)

# ccidをグループ名にマップするための辞書を初期化
ccid2gname = {}

# ths_ceリスト内の閾値ペアをループして処理
for i in range(len(ths_ce) - 1):
    # 現在の閾値を下限、次の閾値を上限として設定
    lower = ths_ce[i]
    upper = ths_ce[i + 1]

    # n_ceが現在の閾値範囲内にある行だけを抽出してdf_qに保存
    df_q = df_cc_nce[(df_cc_nce["n_ce"] >= lower) & (df_cc_nce["n_ce"] < upper)]

    # ccidとグループ名をマッピングする辞書を作成
    gname = f"第{i+1}群(合計{lower}-{upper-1}話)"
    ccid2gname.update({ccid: gname for ccid in df_q["ccid"]})
Hide code cell content
# df_barのccnameに基づきにグループ名をマッピング
df_cc_n4c["gname"] = df_cc_n4c["ccid"].map(ccid2gname)

# gnameとmcnameでdf_barをソート
df_cc_n4c = df_cc_n4c.sort_values(["gname", "mcname"], ignore_index=True)

グループごとに4色カラーを獲得した各話数を合計します。

Hide code cell content
# 4色カラーのデータを集計
# "gname"でグループ化し、"n_4c"の合計と件数を計算
df_color = df_cc_n4c.groupby("gname")["n_4c"].agg(["sum", "count"]).reset_index()
# カラータイプとして"4色カラー"を追加
df_color["color_type"] = "4色カラー"

# モノクロのデータを作成
df_mono = df_color.copy()
# モノクロの各話数は、min_nce * 件数から4色カラーの合計を引いたもの
df_mono["sum"] = df_mono["count"] * min_nce - df_mono["sum"]
# カラータイプに"モノクロ"を追加
df_mono["color_type"] = "モノクロ"

# 4色カラーとモノクロのデータを結合
df_sbar = pd.concat([df_color, df_mono], ignore_index=True)

# 平均話数と割合を計算
df_sbar["mean"] = df_sbar["sum"] / df_sbar["count"]
df_sbar["text"] = df_sbar["mean"].apply(lambda x: f"約{x:0.2}話")

# グループ名とカラータイプでソートし、インデックスをリセット
df_sbar = df_sbar.sort_values(["gname", "color_type"], ignore_index=True)

# 列名をよりわかりやすい名前に変更
df_sbar = df_sbar.rename(
    columns={
        "gname": "グループ名",
        "sum": f"{min_nce}話目までの合計各話数",
        "mean": f"{min_nce}話目までの平均話数",
        "color_type": "掲載形態",
    }
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_sbar.head(10)
グループ名 8話目までの合計各話数 count 掲載形態 8話目までの平均話数 text
0 第1群(合計8-16話) 569 578 4色カラー 0.984429 約0.98話
1 第1群(合計8-16話) 4055 578 モノクロ 7.015571 約7.0話
2 第2群(合計17-31話) 742 594 4色カラー 1.249158 約1.2話
3 第2群(合計17-31話) 4010 594 モノクロ 6.750842 約6.8話
4 第3群(合計32-81話) 814 617 4色カラー 1.319287 約1.3話
5 第3群(合計32-81話) 4122 617 モノクロ 6.680713 約6.7話
6 第4群(合計82-1968話) 830 606 4色カラー 1.369637 約1.4話
7 第4群(合計82-1968話) 4018 606 モノクロ 6.630363 約6.6話
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_sbar, DIR_OUT, "sbar")
DataFrame is saved as '../../data/cm/output/02/props/sbar.csv'.
Hide code cell source
# df_sbarデータフレームを使用して積上げ棒グラフを作成
# f"{min_nce}話目までの平均話数"をx軸に、"グループ名"をy軸に設定
# 棒グラフのモードを"stack"(積み上げ)に設定し、"掲載形態"ごとに色分け
# 各棒には"text"をテキストとして表示
# 色はOKABE_ITOカラーパレットを利用するが、誤解を防ぐためモノクロが黒になるよう並び替え
# 棒グラフを水平方向に描画(orientation="h")
fig = px.bar(
    df_sbar,
    x=f"{min_nce}話目までの平均話数",
    y="グループ名",
    barmode="stack",
    color="掲載形態",
    orientation="h",
    text="text",
    color_discrete_sequence=OKABE_ITO[:2][::-1],
)

# 作成した積上げ棒グラフを表示
show_fig(fig)

上図は、マンガ作品の8話目までの掲載形態(4色カラー、モノクロ)の内訳を、マンガ作品の合計話数の多さに応じてグループ分けして表現した積上げ棒グラフです。 最も合計話数の少ないグループ1から最も多いグループ4にかけて、8話目までに獲得した平均カラー話数が増加していることがわかります。 具体的には、グループ1に属するマンガ作品は平均約1.0回(おそらく第1話のみ)であるのに対し、グループ4に属するマンガ作品は平均約1.4回も4色カラーを獲得していることがわかりました。

「長期連載作品とそうでない作品は、連載開始直後の数話の4色カラー獲得数が異なる」という仮説と整合性のある結果を得られました。

では、マンガ雑誌別に傾向は異なるのでしょうか?

Hide code cell content
# 4色カラーのデータを集計
# "mcname"と"gname"でグループ化し、"n_4c"の合計と件数を計算
df_color = (
    df_cc_n4c.groupby(["mcname", "gname"])["n_4c"].agg(["sum", "count"]).reset_index()
)
# カラータイプとして"4色カラー"を追加
df_color["color_type"] = "4色カラー"

# モノクロのデータを作成
df_mono = df_color.copy()
# モノクロの各話数は、min_nce * 件数から4色カラーの合計を引いたもの
df_mono["sum"] = df_mono["count"] * min_nce - df_mono["sum"]
# カラータイプに"モノクロ"を追加
df_mono["color_type"] = "モノクロ"

# 4色カラーとモノクロのデータを結合
df_sbar2 = pd.concat([df_color, df_mono], ignore_index=True)

# 平均話数と割合を計算
df_sbar2["mean"] = df_sbar2["sum"] / df_sbar2["count"]
df_sbar2["text"] = df_sbar2["mean"].apply(lambda x: f"約{x:0.2}話")

# グループ名とカラータイプでソートし、インデックスをリセット
df_sbar2 = df_sbar2.sort_values(["mcname", "gname", "color_type"], ignore_index=True)

# 列名をよりわかりやすい名前に変更
df_sbar2 = df_sbar2.rename(
    columns={
        "mcname": "マンガ雑誌名",
        "gname": "グループ名",
        "sum": f"{min_nce}話目までの合計各話数",
        "mean": f"{min_nce}話目までの平均話数",
        "color_type": "掲載形態",
    }
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_sbar2.head()
マンガ雑誌名 グループ名 8話目までの合計各話数 count 掲載形態 8話目までの平均話数 text
0 週刊少年サンデー 第1群(合計8-16話) 57 84 4色カラー 0.678571 約0.68話
1 週刊少年サンデー 第1群(合計8-16話) 615 84 モノクロ 7.321429 約7.3話
2 週刊少年サンデー 第2群(合計17-31話) 98 100 4色カラー 0.980000 約0.98話
3 週刊少年サンデー 第2群(合計17-31話) 702 100 モノクロ 7.020000 約7.0話
4 週刊少年サンデー 第3群(合計32-81話) 241 177 4色カラー 1.361582 約1.4話
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_sbar2, DIR_OUT, "sbar2")
DataFrame is saved as '../../data/cm/output/02/props/sbar2.csv'.
Hide code cell source
# df_sbar2データフレームを使用して積上げ棒グラフを作成
# f"{min_nce}話目までの平均話数"をx軸に、"グループ名"をy軸に設定
# 棒グラフのモードを"stack"(積み上げ)に設定し、"掲載形態"ごとに色分け
# 各棒には"text"をテキストとして表示
# "マンガ雑誌名"ごとにファセット(サブプロット)を作成し、2列でラップ
# 色はOKABE_ITOカラーパレットを利用するが、誤解を防ぐためモノクロが黒になるよう並び替え
# 棒グラフを水平方向に描画(orientation="h")
fig = px.bar(
    df_sbar2,
    x=f"{min_nce}話目までの平均話数",
    y="グループ名",
    barmode="stack",
    color="掲載形態",
    orientation="h",
    text="text",
    facet_col="マンガ雑誌名",
    facet_col_wrap=2,
    color_discrete_sequence=OKABE_ITO[:2][::-1],
)

# ファセット(マンガ雑誌ごとの積上げ棒グラフ)のタイトルを簡潔にする処理
# デフォルトではタイトルは「マンガ雑誌名=xxx」という形式になっている
# この処理は「=」で文字列を分割して「xxx」の部分だけを取り出す
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 作成した積上げ棒グラフを表示
show_fig(fig)

上図は、マンガ雑誌別のマンガ作品の8話目までの掲載形態(4色カラー、モノクロ)の内訳を、マンガ作品の合計話数の多さに応じてグループ分けして表現した積上げ棒グラフです。 全体傾向として週刊少年チャンピオン週刊少年マガジンに関しては、長期連載グループほど多くの4色カラーの機会を獲得しているように見えます。 一方で、週刊少年サンデー第3グループが最も多く、週刊少年ジャンプ第2グループが最も多いという結果になりました。 全マンガ雑誌に共通しているのは、第1グループの4色カラー獲得回数が最も少ないという点です。

雑誌ごとの内訳のばらつきの原因を考えるため、平均ではなく合計各話数を表示してみましょう。

Hide code cell source
# df_sbar2データフレームを使用して積上げ棒グラフを作成
# f"{min_nce}話目までの合計各話数"をx軸に、"グループ名"をy軸に設定
# 棒グラフのモードを"stack"(積み上げ)に設定し、"掲載形態"ごとに色分け
# "マンガ雑誌名"ごとにファセット(サブプロット)を作成し、2列でラップ
# 色はOKABE_ITOカラーパレットを利用するが、誤解を防ぐためモノクロが黒になるよう並び替え
# 棒グラフを水平方向に描画(orientation="h")
fig = px.bar(
    df_sbar2,
    x=f"{min_nce}話目までの合計各話数",
    y="グループ名",
    barmode="stack",
    color="掲載形態",
    orientation="h",
    facet_col="マンガ雑誌名",
    facet_col_wrap=2,
    color_discrete_sequence=OKABE_ITO[:2][::-1],
)

# ファセット(マンガ雑誌ごとの積上げ棒グラフ)のタイトルを簡潔にする処理
# デフォルトではタイトルは「マンガ雑誌名=xxx」という形式になっている
# この処理は「=」で文字列を分割して「xxx」の部分だけを取り出す
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 作成した積上げ棒グラフを表示
show_fig(fig)

上図は、マンガ雑誌別のマンガ作品の8話目までの各掲載形態(4色カラー、モノクロ)の合計各話数を、マンガ作品の合計話数の多さに応じてグループ分けして表現した積上げ棒グラフです。 週刊少年サンデー週刊少年ジャンプでは、グループごとの合計各話数に大きな偏りがあることがわかりました。 これは、全マンガ雑誌の集計値を対象にグループ分けの基準となる四分位値を計算したためです。

モザイクプロット#

積上げ棒グラフでは、「長期連載作品とそうでない作品は、連載開始直後の数話の4色カラー獲得数が異なる」という仮説を確認しました。 積上げ棒グラフは内訳を見る際も非常に有効な可視化手法ですが、「棒」によって分母の大きさが異なるとき誤った印象を与えやすいという短所があります。 そこで本項では、モザイクプロットを用いて「長期連載作品とそうでない作品は、連載開始直後の数話の4色カラー獲得数が異なる」という仮説を再度確認します。

モザイクプロットMosaic Plot ) とは、複数の質的変数に対して、その内訳を 長方形の面積 で表した可視化手法です。 その見た目から マリメッコプロットMarimekko Plot) とも呼ばれます。 分割方法を工夫することで三変数以上にも対応可能ですが,よく見かけるのは二変数に対する描画です.

二変数に対するモザイクプロットは、 積上げ棒グラフ の棒の太さを分母の大きさで調整したものと捉えることができます。 これにより、二変数を跨いだ(他の棒中の要素との)比較が可能になりますが、目視で面積を測るのは難しい場合があるので数値を付記すると親切です.

Hide code cell content
# 積上げ棒ブラフと同じデータを利用
df_mos = df_sbar.copy()
Hide code cell content
# 可視化対象のDataFrameを確認
df_mos.head()
グループ名 8話目までの合計各話数 count 掲載形態 8話目までの平均話数 text
0 第1群(合計8-16話) 569 578 4色カラー 0.984429 約0.98話
1 第1群(合計8-16話) 4055 578 モノクロ 7.015571 約7.0話
2 第2群(合計17-31話) 742 594 4色カラー 1.249158 約1.2話
3 第2群(合計17-31話) 4010 594 モノクロ 6.750842 約6.8話
4 第3群(合計32-81話) 814 617 4色カラー 1.319287 約1.3話
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_mos, DIR_OUT, "mos")
DataFrame is saved as '../../data/cm/output/02/props/mos.csv'.
Hide code cell source
# df_mosを用いてモザイクプロットを作成
# x軸にはグループ名、y軸には8話目までの平均話数、色分けは掲載形態を指定
# 各セルの幅はcount列に基づき、各セルにはテキスト情報も表示
# 色はOKABE_ITOカラーパレットを利用するが、誤解を防ぐためモノクロが黒になるよう並び替え
fig = create_mosaicplot(
    df_mos,
    x="グループ名",
    y="8話目までの平均話数",
    color="掲載形態",
    width="count",
    text="text",
    color_discrete_sequence=OKABE_ITO[:2][::-1],
)

# 作成したモザイクプロットを表示
show_fig(fig)

上図は、マンガ作品の8話目までの掲載形態(4色カラー、モノクロ)の内訳を、マンガ作品の合計話数の多さに応じてグループ分けして表現したモザイクプロットです。 棒の太さは各グループの合計マンガ作品数と対応しています。 積上げ棒グラフと比較して、X軸とY軸が反転していることに注意してください。

積上げ棒グラフでは内訳か絶対量のいずれかしか可視化できませんでしたが、モザイクプロットではその両方を同時に可視化することができます。 ただし、絶対量は長方形の 面積 で表現しているため、目視での比較は困難であることに注意が必要です。

合計各話数が大きいグループほど、平均的に多くのカラー各話を獲得していることがわかります。 「長期連載作品とそうでない作品は、連載開始直後の数話の4色カラー獲得数が異なる」という仮説と整合性のある結果を得られました。

では、マンガ雑誌内ではどうでしょうか? ここでは、積上げ棒グラフで特徴的な結果となった週刊少年ジャンプを同様に可視化してみましょう。

Hide code cell content
# 雑誌別のグループごとのカラー各話数を集計したdf_sbar2を利用
df_mos2 = df_sbar2.copy()

# df_mos2からマンガ雑誌名が「週刊少年ジャンプ」のデータのみを抽出
df_mos2 = df_mos2[df_mos2["マンガ雑誌名"] == "週刊少年ジャンプ"]
Hide code cell content
# 可視化対象のDataFrameを確認
df_mos2.head()
マンガ雑誌名 グループ名 8話目までの合計各話数 count 掲載形態 8話目までの平均話数 text
8 週刊少年ジャンプ 第1群(合計8-16話) 291 225 4色カラー 1.293333 約1.3話
9 週刊少年ジャンプ 第1群(合計8-16話) 1509 225 モノクロ 6.706667 約6.7話
10 週刊少年ジャンプ 第2群(合計17-31話) 296 166 4色カラー 1.783133 約1.8話
11 週刊少年ジャンプ 第2群(合計17-31話) 1032 166 モノクロ 6.216867 約6.2話
12 週刊少年ジャンプ 第3群(合計32-81話) 147 91 4色カラー 1.615385 約1.6話
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_mos2, DIR_OUT, "mos2")
DataFrame is saved as '../../data/cm/output/02/props/mos2.csv'.
Hide code cell source
# x軸にはグループ名、y軸には8話目までの平均話数、色分けは掲載形態を指定
# 各セルの幅はcount列に基づき、各セルにはテキスト情報も表示
# 色はOKABE_ITOカラーパレットを利用するが、誤解を防ぐためモノクロが黒になるよう並び替え
fig = create_mosaicplot(
    df_mos2,
    x="グループ名",
    y="8話目までの平均話数",
    color="掲載形態",
    width="count",
    text="text",
    color_discrete_sequence=OKABE_ITO[:2][::-1],
)

# 作成したモザイクプロットを表示
show_fig(fig)

上図は、週刊少年ジャンプに掲載されたマンガ作品の8話目までの掲載形態(4色カラー、モノクロ)の内訳を、マンガ作品の合計話数の多さに応じてグループ分けして表現したモザイクプロットです。 棒の太さは各グループの合計マンガ作品数と対応しています。

全マンガ雑誌の集計結果と異なり、第3グループ第4グループで平均的なカラー各話数が下がっています。 そこで棒の太さを確認すると、特に第3グループに属するマンガ作品数が極端に少ないことがわかります。 各グループのサンプルサイズの偏りが原因と断定することはできませんが、これを足がかりに次の分析を設計することはできます。

積上げ棒グラフでは内訳と絶対量で2回可視化をしなければたどり着けなかった結論に、モザイクプロットは1回でたどり着くことが可能です。 絶対量を 面積 で表現しているという扱いづらさはありますが、探索的データ分析の手札の一つとして覚えておくと良いでしょう。

積上げ密度プロット#

次は、多くの4色カラー各話を掲載した作品について分析してみましょう。 そこで本項では積上げ密度プロットを用い、「多くの4色カラー各話を掲載したマンガ作品の中でも、年間獲得数の推移が異なる」という仮説を確かめます。

積上げ密度プロットStacked Density Plot ) は、質的変数の内訳の推移を 面積 で表現する可視化手法です。 エリアプロットArea Plot )と呼ばれることもあります。横軸に質的変数(例えば日付)、縦軸に質的変数の内訳あるいは絶対量を取ります。 詳細は7章を参照ください。

Hide code cell content
# 上位n_ccのマンガ作品を選択するための定数
n_cc = 5
Hide code cell content
# "ccid"でグループ化し、"four_colored"列の合計を計算
df_cc_n4c = df_ce.groupby(["ccid"])["four_colored"].sum().reset_index(name="n_4c")

# 4色カラーの合計数で降順にソートし、インデックスをリセット
df_cc_n4c = df_cc_n4c.sort_values("n_4c", ascending=False, ignore_index=True)

# 上位n_cc(5つ)のccidを選択し、リストとして取得
ccids = df_cc_n4c["ccid"].head(n_cc).to_list()
Hide code cell content
# 'date'列を日付型に変換し、その年部分を新しい'year'列として追加
df_ce["year"] = pd.to_datetime(df_ce["date"]).dt.year

# 各マンガ作品名と年ごとに4色カラーの合計、件数、平均を集計
df_cc_year_n4c = (
    df_ce.groupby(["ccname", "ccid", "year"])["four_colored"]
    .agg(["sum", "count", "mean"])
    .reset_index()
)
Hide code cell content
# ccid列がccidsリストに含まれている行のみを選択し、df_colorとして再定義
df_color = df_cc_year_n4c[df_cc_year_n4c["ccid"].isin(ccids)].reset_index(drop=True)
# color_type列に4色カラーを設定
df_color["color_type"] = "4色カラー"

# モノクロのデータを作成
df_mono = df_color.copy()
# モノクロの各話数は、合計各話数から4色カラーの合計を引いたもの
df_mono["sum"] = df_mono["count"] - df_mono["sum"]
# モノクロの平均は1から4色カラーの平均を引いたもの
df_mono["mean"] = 1 - df_mono["mean"]
# カラータイプに"モノクロ"を設定
df_mono["color_type"] = "モノクロ"

# 4色カラーとモノクロのデータを結合
df_area = pd.concat([df_color, df_mono], ignore_index=True)
# 'ccid'列をカテゴリ型に変換し、ccidsリストに基づいて順序付け
df_area["ccid"] = pd.Categorical(df_area["ccid"], categories=ccids, ordered=True)
# 'year', 'ccid', 'color_type'の順でソートし、インデックスをリセット
df_area = df_area.sort_values(["year", "ccid", "color_type"], ignore_index=True)
Hide code cell content
# px.areaで欠損値を自動補完されないように0埋めする処理

# df_areaからユニークなccnamesのリストを取得
ccnames = df_area["ccname"].unique()
# df_areaに含まれる年の範囲を取得(最小年から最大年まで)
years = range(df_area["year"].min(), df_area["year"].max() + 1)
# df_areaからユニークなcolor_type(色のタイプ)のリストを取得
color_types = df_area["color_type"].unique()

# ccname, year, color_typeの組み合わせごとにダミーの行を作成
# これによって、すべての可能な組み合わせをカバーする
df_dummy = pd.DataFrame(
    [
        {"ccname": ccname, "year": year, "color_type": ct}
        for ccname, year, ct in itertools.product(ccnames, years, color_types)
    ]
)

# 元のdf_areaとダミーデータフレームdf_dummyを結合
# 結合の基準はccname, year, color_typeの各カラム
df_area = pd.merge(df_area, df_dummy, on=["ccname", "year", "color_type"], how="outer")
# sumおよびmean列のNaNを0埋め
df_area["sum"] = df_area["sum"].fillna(0)
df_area["mean"] = df_area["mean"].fillna(0)
Hide code cell content
# 列名をよりわかりやすい名前に変更
df_area = df_area.rename(
    columns={
        "ccname": "マンガ作品名",
        "year": "掲載年",
        "sum": "各話数",
        "color_type": "掲載形態",
    }
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_area.head()
マンガ作品名 ccid 掲載年 各話数 count mean 掲載形態
0 ドカベン C95127 1972 22.0 34.0 0.647059 4色カラー
1 ドカベン C95127 1972 12.0 34.0 0.352941 モノクロ
2 ドカベン C95127 1973 21.0 49.0 0.428571 4色カラー
3 ドカベン C95127 1973 28.0 49.0 0.571429 モノクロ
4 ドカベン C95127 1974 19.0 49.0 0.387755 4色カラー
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_area, DIR_OUT, "area")
DataFrame is saved as '../../data/cm/output/02/props/area.csv'.
Hide code cell source
# df_areaデータフレームを使用して積上げ密度プロットを作成
# "掲載年"をx軸に、"各話数"をy軸に設定
# "掲載形態"ごとに色分けし、"マンガ作品名"ごとにファセット(サブプロット)を作成
# facet_col_wrap=1で各ファセットを1列に配置
# 色はOKABE_ITOカラーパレットの最初の2色を逆順で使用
fig = px.area(
    df_area,
    x="掲載年",
    y="各話数",
    color="掲載形態",
    facet_col="マンガ作品名",
    facet_col_wrap=1,
    color_discrete_sequence=OKABE_ITO[:2][::-1],
)

# hovermodeをx unifiedにすることで、同じxの値に対する全てのyの値を表示
fig.update_layout(hovermode="x unified")

# 各サブプロットの注釈(ファセットタイトル)を更新して、"マンガ作品名"の値のみ表示
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 作成した積上げ密度プロットを表示
show_fig(fig)

上図は、マンガ作品の掲載形態(4色カラー、モノクロ)の合計各話数の推移を表現した積上げ密度プロットです。 1年ごとに各掲載形態を集計しています。 可視化対象のマンガ作品として、最も4色カラーの各話数が多い上位5作品を選定しました。

マンガ作品によって特徴が現れており興味深いです。 例えばドカベンは、断続的に掲載されていますが、掲載期間中は20-40%程度の割合で4色カラーを獲得しています。 こちら葛飾区亀有公園前派出所はコンスタントに各話を掲載していますが、4色カラーを獲得する割合は多くありません。 2011年に14回も4色カラーを獲得していますが、このタイミングは同作を原作とした実写ドラマの映画公開年と一致しています。 ONE PIECEは前述した2作品と比較して掲載期間は短いですが、毎年コンスタントに5回以上カラーを獲得しています。 範馬刃牙 SON OF OGREは更に掲載期間が短いですが、特に序盤において、非常に高い4色カラー獲得率を誇っています。 最後に弱虫ペダルですが、こちらも連載開始以降毎年15回以上カラーを獲得しています。

「多くの4色カラー枠を獲得したマンガ作品の中でも、年間獲得数の推移が異なる」という仮説と整合性のある結果を得られました。

ツリーマップ#

積上げ密度プロットの可視化例を見て違和感を感じた方もいるかもしれません。 カラー各話数の多いマンガ作品は、全て週刊少年ジャンプ週刊少年チャンピオンに属するものであり、週刊少年サンデー週刊少年マガジンに属するものは一つもありませんでした。 なお、マンガ雑誌間において、雑誌巻号数に大きな違いはありません。 毎年50号前後がデータとして格納されています。 ではなぜ特定のマンガ雑誌の作品ばかりが、特にカラー各話数の多いマンガ作品として列挙されてしまうのでしょうか? 考えられる原因は二つあります:

  • マンガ雑誌によって、カラー各話の総数自体が異なる

  • マンガ雑誌によって、カラー各話を配分するマンガ作品の偏り具合が異なる

誌面構成や編集方針の違いから、カラー各話を多用する雑誌とそうでない雑誌が存在する可能性があります。 先述したようにカラーページはコストがかかります。 雑誌によっては巻頭にグラビアや広告を掲載することもあるため、マンガ作品以外とのバランスで、マンガ作品に割り当てるカラーページに差が生じてしまう可能性はあります。

後者に関しても編集方針と強い関わりがあります。 極論を言えば、人気作品に優先してカラーページを割り当てる戦略もあれば、可能な限り全てのマンガ作品に公平にカラーページを割り当てる戦略もあります。 (どちらが良いという話ではなく)マンガ雑誌によって多様な戦略があり得ますので、カラー各話数の偏りに差が生じる可能性があります。

そこで本項では、ツリーマップを用いて上記二つの仮説を確認します。 ツリーマップTree Map) とは、 階層構造(ツリー構造)を持つ 質的変数に対して、その内訳を 長方形の面積 で表現する可視化手法です。 詳細は7章を参照ください。

Hide code cell content
# df_cc_nceからユニークなccidのリストを作成
ccids = list(df_cc_nce["ccid"].unique())

# df_ceからccid列がccidsリストに含まれ、four_coloredがTrueのデータのみを抽出
# reset_index(drop=True)を使って、インデックスをリセット(古いインデックスを削除)
df_tree = df_ce[df_ce["ccid"].isin(ccids) & df_ce["four_colored"]].reset_index(
    drop=True
)

# 必要な列のみ保持
df_tree = df_tree[["mcname", "ccname", "cename", "four_colored"]]
Hide code cell content
# 可視化対象のDataFrameを確認
df_tree.head()
mcname ccname cename four_colored
0 週刊少年マガジン ダイヤのA 第238話/この世代 True
1 週刊少年マガジン 我間乱 ~GAMARAN~ 第94話 True
2 週刊少年マガジン ファイ・ブレイン 最期のパズル 第1話 クラシック同好会 True
3 週刊少年マガジン かってに改蔵 特別番外編 「損して得とれない」 True
4 週刊少年マガジン FAIRY TAIL 第231話 終わらせる者 True
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_tree, DIR_OUT, "tree")
DataFrame is saved as '../../data/cm/output/02/props/tree.csv'.
Hide code cell source
# df_treeを使ってツリーマップを作成
# 階層は"all" → "mcname" → "ccname"の順であり、サイズは"four_colored"の値によって決定
# color_discrete_sequenceで色の配列を設定(OKABE_ITO配色を使用)
fig = px.treemap(
    df_tree,
    path=[px.Constant("all"), "mcname", "ccname"],
    values="four_colored",
    color_discrete_sequence=OKABE_ITO,
)

# ツリーマップの根元の色を薄灰色に設定
fig.update_traces(root_color="lightgrey")

# 作成したツリーマップを表示
show_fig(fig)

上図は、マンガ雑誌・マンガ作品ごとのカラー各話獲得回数の内訳を表現したツリーマップです。 長方形の面積が、カラー各話回数を表現しています。

まず、マンガ雑誌によってカラー各話の総数が大きく異なることがわかります。 週刊少年ジャンプ週刊少年チャンピオンが比較的多く、週刊少年サンデー週刊少年マガジンが比較的少ないように見えます[7]。 「マンガ雑誌によって、カラー各話の総数自体が異なる」という仮説と整合性のある結果を得られました。

次に、各誌の内訳の偏り具合を見てみましょう。 特に目を引くのは、週刊少年チャンピオンではないでしょうか。 他のマンガ作品と比較して、明らかにドカベン弱虫ペダルそして範馬刃牙 SON OF OGREのカラー各話回数が多いことがわかります。 一方で、週刊少年サンデーは、史上最強の弟子シンイチがトップでありながら、その他のマンガ作品と大きな差はないように見えます。 以上から「マンガ雑誌によって、カラー各話を配分するマンガ作品の偏り具合が異なる」という仮説と整合性のある結果を得られました。

パラレルセットグラフ#

突然ですが、みなさんは何曜日が好きですか? 筆者も会社員である以上、金曜日の地位は不動ですが、それ以外ではマンガ雑誌が発売される曜日が好きです。 極端な話、マンガファンにとってマンガ雑誌の発売スケジュールは、日常生活に影響を与える[周平, 2020]ほど大きな意味を持ちます。

筆者が購読を始めて以来、各マンガ雑誌の発売曜日は(祝日等の特別な場合を除き)基本的に固定されているように感じます。 では、創刊以来ずっと発売曜日は固定されているのでしょうか? マンガファンの先輩方の曜日感覚は現在と全く同じだったのでしょうか? 本項ではパラレルセットグラフを用いて、「マンガ雑誌の発売曜日は基本的に変わらない」という仮説を確認します。

パラレルセットグラフParallel Set Graph ) とは、複数の質的変数に対して、それらの内訳を 平行棒の面積 (あるいは 長さ )で表現する可視化手法です。 三つ以上の質的変数に対しても適用可能であるという利点があります。 パラレルセットグラフを作成する上でのポイントは、最も強調したい質的変数を左側に配置し、かつ色付けすることです。 詳細は7章を参照ください。

Hide code cell content
# df_ceからminameが重複していないデータのみを抽出し、インデックスをリセット
df_par = df_ce.drop_duplicates(subset="miname").reset_index(drop=True)

# 年代情報をdf_parに追加
df_par = add_years_to_df(df_par)
# 曜日情報をdf_parに追加
df_par = add_weekday_to_df(df_par)
# 曜日、年代、mcname(雑誌名)の順でデータをソートし、インデックスをリセット
df_par = df_par.sort_values(["weekday", "years", "mcname"], ignore_index=True)

# 可視化用に保持するカラム
cols2rename = {"mcname": "雑誌名", "years": "年代", "weekday_str": "発売曜日", "weekday":"weekday"}
# カラム名をよりわかりやすい名前に変更
df_par = format_cols(df_par, cols2rename)
Hide code cell content
# 可視化対象のDataFrameを確認
df_par.head()
雑誌名 年代 発売曜日 weekday
0 週刊少年サンデー 1970 0
1 週刊少年サンデー 1970 0
2 週刊少年ジャンプ 1970 0
3 週刊少年ジャンプ 1970 0
4 週刊少年ジャンプ 1970 0
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_par, DIR_OUT, "par")
DataFrame is saved as '../../data/cm/output/02/props/par.csv'.
Hide code cell source
# df_parを使ってパラレルセットグラフを作成
# このプロットは「発売曜日」、「雑誌名」、「年代」を次元として可視化する
# 色分けは曜日('weekday')に基づいており、カラースケールはOKABE_ITOの最初の7色を使用
fig = px.parallel_categories(
    df_par,
    dimensions=["発売曜日", "雑誌名", "年代"],
    color="weekday",
    color_continuous_scale=OKABE_ITO[:7],
)

# カラースケールを非表示に設定
fig.update_coloraxes(showscale=False)

# 作成したパラレルセットグラフを表示
show_fig(fig)

上図は、発売曜日・雑誌名・年代ごとの雑誌巻号数の内訳を表したパラレルセットグラフです。 ここでは特に 各発売曜日 を強調するために配色し、各マンガ雑誌、各年代における内訳を表現しています。

発売曜日に関しては、曜日と曜日が特に多く、曜日曜日がそれに続きます。 曜日は非常に少なく、曜日と曜日に至ってはほとんど発売されていません。

次に各マンガ雑誌の内訳を見てみましょう。 週刊少年サンデー曜日と曜日[8]に発売された実績があります。年代列に目を向けると、1970年代は曜日に発売されていたようですが、1980年代以降曜日発売が定着したようです。 週刊少年ジャンプは全期間を通して基本的に曜日に発売されており、例外は非常に少ないことがわかります。 週刊少年チャンピオン曜日と曜日と曜日の発売実績があります。年代としては、1970年代に曜日に発売されており、1980年代に曜日に移行し、1990年代以降曜日に落ち着いたようです。 週刊少年マガジンは、1970年代のみ曜日に発売され、1980年代以降は曜日に発売されるようになりました。

最後に、年代を中心に内訳を見てみましょう。 1970年代は、曜発売と曜発売のマンガ雑誌が半々でした[9]1980年代に入ってから分散し、そして曜日に発売されるようになりました。 そして1990年代以降は現在と同じ発売曜日に固定されたようです。

以上から「マンガ雑誌の発売曜日は基本的に変わらない」という仮説は誤りであり、発売曜日が固定されたのは1990年代以降であることがわかりました。

parallel_categoriesの色指定

parallel_categoriesメソッドではcolor_discrete_sequenceを引数として取らず、color_continuous_scaleを引数として受け取ります。 OKABE_ITOをそのまま引数としてわたすと補間されてしまうため、ここでは7色分を渡すことで意図通りの配色となるよう調整しています。